﻿//PlazaDepthBuilder.cs
//Copyright (c) 2013 StockSharp LLC, all rights reserved.
//This code module is part of StockSharp library.
//This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3.
//See the file License.txt for the license details.
//More info on: http://stocksharp.com

namespace StockSharp.Plaza
{
	using System;
	using System.Collections.Generic;
	using System.Linq;

	using Ecng.Collections;
	using Ecng.Common;

	using StockSharp.BusinessEntities;
	using StockSharp.Plaza.Metadata;
	using StockSharp.Algo;

	class PlazaDepthBuilder
	{
		private sealed class QuoteInfo
		{
			public QuoteInfo(long replRev, Quote quote)
			{
				ReplRev = replRev;
				Quote = quote;
			}

			public long ReplRev { get; private set; }
			public Quote Quote { get; private set; }
		}

		private sealed class DepthInfo
		{
			private DateTime _lastChangeTime;
			private readonly MarketDepth _pendingDepth;

			public DepthInfo(MarketDepth pendingDepth)
			{
				if (pendingDepth == null)
					throw new ArgumentNullException("pendingDepth");

				Quotes = new Dictionary<long, QuoteInfo>();

				_pendingDepth = pendingDepth;
				_lastChangeTime = pendingDepth.LastChangeTime;
			}

			public Dictionary<long, QuoteInfo> Quotes { get; private set; }

			public void UpdateTime(DateTime time)
			{
				// В соответствии с http://forum.rts.ru/viewtopic.asp?t=16922
				// у следующей записи может быть moment < moment предыдущей записи
				// например когда обновляется объем у старого "уровня" в стакане.
				// т.о. в сценарии когда в стакане обновляются уровни, а не добавляются новые
				// время последнего обновления получить из только этого потока репликации невозможно.
				// в качестве временной меры за время стакана берется максимальное значение moment 
				// для данного инструмента/

				_lastChangeTime = (time - _lastChangeTime) >= TimeSpan.FromMilliseconds(1) ? time : _lastChangeTime.AddMilliseconds(1);
			}

			public void ApplyChanges(MarketDepth realDepth)
			{
				// http://forum.rts.ru/viewtopic.asp?p=101663
				// Может быть одна строка стакана (одна цена) но два разных replId. 
				// Они дублируют друг друга и возникают когда заявка входит-выходит 
				// за границу в 5 котировок
				// Поэтому здесь агрегируем по цене. При этом иногда возникают ситуации что
				// после обработки события дефакто стакан не изменился. Они отфильтровываются ниже (MD.GetDelta)

				var quoteMap = new Dictionary<decimal, Quote>();

				foreach (var pair in Quotes)
				{
					var quote = pair.Value.Quote;

					quoteMap[quote.Price] = quote;

					if (quote.Volume == 0)
						throw new InvalidOperationException("Котировка для {0} имеет нулевой объем.".Put(quote.Security));
				}

				var quotes = quoteMap.Values.ToArray();

				_pendingDepth.Update(quotes, _lastChangeTime);

				if (_pendingDepth.Bids.Length > 0 && _pendingDepth.Asks.Length > 0 && _pendingDepth.BestBid.Price >= _pendingDepth.BestAsk.Price)
					throw new InvalidOperationException("Цена лучшего бида больше или равна цене лучшего оффера для инструмента {0}.".Put(_pendingDepth.Security));

				realDepth
					.Update(_pendingDepth.Bids, _pendingDepth.Asks, true, _pendingDepth.LastChangeTime)
					.InitLatency();
			}
		}

		private sealed class TableInfo
		{
			public readonly HashSet<MarketDepth> LastChangedDepths = new HashSet<MarketDepth>();
			public readonly Dictionary<Security, DepthInfo> Depths = new Dictionary<Security, DepthInfo>();

			public void Clear()
			{
				LastChangedDepths.Clear();
				Depths.Clear();
			}
		}

		private readonly PlazaTrader _trader;

		// mika так как данные по стакану изменяются одним потоком подключения, то синхронизованные коллекции использовать не нужно
		private readonly Dictionary<PlazaTable, TableInfo> _info = new Dictionary<PlazaTable, TableInfo>();

		public PlazaDepthBuilder(PlazaTrader trader)
		{
			if (trader == null)
				throw new ArgumentNullException("trader");

			_trader = trader;

			_info.SafeAdd(trader.TableRegistry.Aggregation5Future);
			_info.SafeAdd(trader.TableRegistry.Aggregation20Future);
			_info.SafeAdd(trader.TableRegistry.Aggregation50Future);
			_info.SafeAdd(trader.TableRegistry.Aggregation5Option);
			_info.SafeAdd(trader.TableRegistry.Aggregation20Option);
			_info.SafeAdd(trader.TableRegistry.Aggregation50Option);
			_info.SafeAdd(trader.TableRegistry.McxOrderBookSpot);
			_info.SafeAdd(trader.TableRegistry.McxOrderBookCurrency);
		}

		private TableInfo GetInfo(PlazaTable table)
		{
			return _info[table];
		}

		public void ClearQuotes(PlazaTable table)
		{
			GetInfo(table).Clear();
		}

		public void UpdateQuote(PlazaTable table, PlazaRecord record, Security security, decimal price, decimal volume, OrderDirections direction, DateTime time)
		{
			var quote = new Quote
			{
				Security = security,
				Price = price,
				Volume = volume,
				OrderDirection = direction
			};

			var realDepth = _trader.GetMarketDepth(security);

			var tableInfo = GetInfo(table);

			var info = tableInfo.Depths.SafeAdd(security, key => new DepthInfo(realDepth.Clone()));

			if (!record.ReplDeleted)
			{
				// Судя по этой ветке http://forum.rts.ru/viewtopic.asp?t=16922
				// В безбазовом клиенте запись нужно удалять, если и объем, и цена равны нулю.
				if (quote.Volume != 0 && quote.Price != 0)
					info.Quotes[record.ReplId] = new QuoteInfo(record.ReplRevision, quote);
				else
					info.Quotes.Remove(record.ReplId);
			}
			else
			{
				// P2ClientFate.doc - стр. 16
				// replAct — признак того, что запись удалена. При удалении записи на сервере в поле replAct заносится значение ее replID. 
				// Если replAct = 0 — запись активна (не удалена)
				// Встречается только в БАЗОВОМ клиенте

				info.Quotes.Remove(record.ReplId);
			}

			info.UpdateTime(time);

			tableInfo.LastChangedDepths.Add(realDepth);
		}

		public void RemoveQuotesByRev(PlazaTable table, long revision)
		{
			var tableInfo = GetInfo(table);

			foreach (var pair in tableInfo.Depths)
			{
				var security = pair.Key;
				var realDepth = _trader.GetMarketDepth(security);
				var depthInfo = tableInfo.Depths.SafeAdd(security, key => new DepthInfo(realDepth.Clone()));
				
				depthInfo.Quotes.RemoveWhere(qs => qs.Value.ReplRev < revision);
				depthInfo.UpdateTime(security.GetMarketTime());

				// mika
				// принудительно добавляет стакан как измененный, так как DatumDeleted вызывается вне транзакции 
				// http://forum.rts.ru/viewtopic.asp?p=126849#126849
				//
				tableInfo.LastChangedDepths.Add(realDepth);
			}
		}

		public IEnumerable<MarketDepth> FlushChanges(PlazaTable table)
		{
			var tableInfo = GetInfo(table);

			foreach (var realDepth in tableInfo.LastChangedDepths)
			{
				tableInfo.Depths[realDepth.Security].ApplyChanges(realDepth);
			}

			return tableInfo.LastChangedDepths.CopyAndClear();
		}
	}
}
